`timescale 1ns / 1ps
/**
	@file l2cache.v
	@author griffin.milsap@gmail.com
	@brief An L2 cache with two sides (I-SIDE/D-SIDE)
**/
module l2cache(
	clk,
	l1_iside_addr, l1_iside_rd, l1_iside_wr, l1_iside_dout, l1_iside_wmask, l1_iside_din, l1_iside_doutok, // ISIDE L1 Bus
	l1_dside_addr, l1_dside_rd, l1_dside_wr, l1_dside_dout, l1_dside_wmask, l1_dside_din, l1_dside_doutok, // DSIDE L1 Bus
	mmu_addr, mmu_rd, mmu_wr, mmu_dout, mmu_wmask, mmu_din, mmu_dinok							           // MMU Bus
	);
	
	input wire clk;
	
	// ISIDE L1 Bus
	input wire[31:0] l1_iside_addr;
	input wire l1_iside_rd;
	input wire l1_iside_wr;
	output reg[31:0] l1_iside_dout = 0;
	input wire[3:0] l1_iside_wmask;
	input wire[31:0] l1_iside_din;
	output reg l1_iside_doutok = 0;
	
	// DSIDE L1 Bus
	input wire[31:0] l1_dside_addr;
	input wire l1_dside_rd;
	input wire l1_dside_wr;
	output reg[31:0] l1_dside_dout = 0;
	input wire[3:0] l1_dside_wmask;
	input wire[31:0] l1_dside_din;
	output reg l1_dside_doutok = 0;
	
	// MMU Bus
	output reg[31:0] mmu_addr = 0;
	output reg mmu_rd = 0;
	output reg mmu_wr = 0;
	output reg[31:0] mmu_dout = 0;
	output reg[3:0] mmu_wmask = 0;
	input wire[31:0] mmu_din;
	input wire mmu_dinok;
	
	reg[31:0] mmu_din_buf = 0;
	reg mmu_dinok_buf = 0;
	always @(posedge clk) begin
		$display("[L2] CLOCK!");
		
		// Feed ISIDE Upstream inputs
		iside_up_rd <= l1_iside_rd;
		iside_up_wr <= l1_iside_wr;
		iside_up_mask <= l1_iside_wmask;
		iside_up_addr <= l1_iside_addr;
		iside_up_din <= l1_iside_din;
		
		// Feed ISIDE Upstream Outputs
		l1_iside_dout <= iside_up_dout;
		l1_iside_doutok <= iside_up_doutok;
		
		// Feed DSIDE Upstream Inputs
		dside_up_rd <= l1_dside_rd;
		dside_up_wr <= l1_dside_wr;
		dside_up_mask <= l1_dside_wmask;
		dside_up_addr <= l1_dside_addr;
		dside_up_din <= l1_dside_din;
		
		// Feed DSIDE Upstream Outputs
		l1_dside_dout <= dside_up_dout;
		l1_iside_doutok <= dside_up_doutok;
		
		// Buffer inputs from downstream
		mmu_din_buf <= mmu_din;
		mmu_dinok_buf <= mmu_dinok;
	end
	
	// Handle queueing of output to MMU
	reg dn_pending = 0;
	reg dn_connect = 0; // 0 for ISIDE, 1 for DSIDE
	reg dn_queued = 0;
	
	// Buffering/Queueing of operation, address, and data
	reg[31:0] dn_addr_buf[1:0];
	reg dn_rd_buf[1:0];
	reg dn_wr_buf[1:0];
	reg[31:0] dn_dout_buf[1:0];
	reg[3:0] dn_mask_buf[1:0];
	
	always @(iside_dn_rd, iside_dn_wr, dside_dn_rd, dside_dn_wr) begin
	
		// Already have a pending operation on dside, queue iside op.
		if(dn_pending & (iside_dn_rd | iside_dn_wr)) begin
			dn_queued <= 1;
			dn_addr_buf[1] <= iside_dn_addr;
			dn_rd_buf[1] <= iside_dn_rd;
			dn_wr_buf[1] <= iside_dn_wr;
			dn_dout_buf[1] <= iside_dn_dout;
			dn_mask_buf[1] <= iside_dn_mask;
		end
		
		// Already have a pending operation on iside, queue dside op.
		if(dn_pending & (dside_dn_rd | dside_dn_wr)) begin
			dn_queued <= 1;
			dn_addr_buf[1] <= dside_dn_addr;
			dn_rd_buf[1] <= dside_dn_rd;
			dn_wr_buf[1] <= dside_dn_wr;
			dn_dout_buf[1] <= dside_dn_dout;
			dn_mask_buf[1] <= dside_dn_mask;
		end
		
		// No pending operations, start an iside op.
		if(!dn_pending & (iside_dn_rd | iside_dn_wr)) begin
			dn_pending <= 1;
			dn_connect <= 0;
			dn_addr_buf[0] <= iside_dn_addr;
			dn_rd_buf[0] <= iside_dn_rd;
			dn_wr_buf[0] <= iside_dn_wr;
			dn_dout_buf[0] <= iside_dn_dout;
			dn_mask_buf[0] <= iside_dn_mask;			
		end
		
		// No pending operations, start a dside op.
		else if(!dn_pending & (dside_dn_rd | dside_dn_wr)) begin
			dn_pending <= 1;
			dn_connect <= 1;
			dn_addr_buf[0] <= dside_dn_addr;
			dn_rd_buf[0] <= dside_dn_rd;
			dn_wr_buf[0] <= dside_dn_wr;
			dn_dout_buf[0] <= dside_dn_dout;
			dn_mask_buf[0] <= dside_dn_mask;
		end
	end
	
	// Cancel dn_pending/dn_queued with conditions
	always @(l1_iside_doutok, l1_dside_doutok, mmu_wr) begin
		
		// If we have a pending operation, determine if its completed.
		if(dn_pending) begin
			
			// If current op is a read and either the iside or dside have output, we're done
			if(dn_rd_buf[0] & (l1_iside_doutok | l1_dside_doutok)) begin
			
				// If there is a queued op, dequeue it, but keep dn_pending state
				if(dn_queued) begin
					dn_addr_buf[0] <= dn_addr_buf[1];
					dn_rd_buf[0] <= dn_rd_buf[1];
					dn_wr_buf[0] <= dn_wr_buf[1];
					dn_dout_buf[0] <= dn_dout_buf[1];
					dn_mask_buf[0] <= dn_mask_buf[1];
					dn_connect <= ~dn_connect; // connect other side to mmu
					dn_queued <= 0;
				end 
				else // There is no longer a pending operation
					dn_pending <= 0;
			end
			
			// If the current op is a write, and MMU_WR has been issued, we're done.
			if(dn_wr_buf[0] & mmu_wr) begin
			
				// If there is a queued op, dequeue it, but keep dn_pending state
				if(dn_queued) begin
					dn_addr_buf[0] <= dn_addr_buf[1];
					dn_rd_buf[0] <= dn_rd_buf[1];
					dn_wr_buf[0] <= dn_wr_buf[1];
					dn_dout_buf[0] <= dn_dout_buf[1];
					dn_mask_buf[0] <= dn_mask_buf[1];
					dn_connect <= ~dn_connect;
					dn_queued <= 0;
				end
				else // There is no longer a pending operation
					dn_pending <= 0;
			end
		end
	end
	
	// If a downward operation is pending, connect the outputs
	always @(dn_pending, dn_connect, mmu_din_buf, mmu_dinok_buf,
		dn_addr_buf[0], dn_rd_buf[0], dn_wr_buf[0], dn_dout_buf[0], dn_mask_buf[0]) begin
		
		// By default, nothing is connected to the outputs
		mmu_addr <= 0;
		mmu_rd <= 0;
		mmu_wr <= 0;
		mmu_dout <= 0;
		mmu_wmask <= 0;
		
		// But if we have a downstream pending operation,
		if(dn_pending) begin
		
			// Connect the current operation
			mmu_addr <= dn_addr_buf[0];
			mmu_rd <= dn_rd_buf[0];
			mmu_wr <= dn_wr_buf[0];
			mmu_dout <= dn_dout_buf[0];
			mmu_wmask <= dn_mask_buf[0];
			
			// Feed inputs from MMU to appropriate cache
			if(dn_connect == 0) begin
				iside_dn_din <= mmu_din;
				iside_dn_dinok <= mmu_dinok;
			end
			else if(dn_connect == 1) begin
				dside_dn_din <= mmu_din;
				dside_dn_dinok <= mmu_dinok;
			end
		end
		
	end
	
	// ISIDE CACHE
	reg iside_up_rd;
	reg iside_up_wr;
	reg[3:0] iside_up_mask;
	reg[31:0] iside_up_addr;
	reg[31:0] iside_up_din;
	wire[31:0] iside_up_dout;
	wire[31:0] iside_up_doutok;
	wire iside_dn_rd;
	wire iside_dn_wr;
	wire[3:0] iside_dn_mask;
	reg[31:0] iside_dn_din;
	wire[31:0] iside_dn_addr;
	wire[31:0] iside_dn_dout;
	reg[31:0] iside_dn_dinok;
	twosetcache iside(
		.clk(clk),
		.up_rd(iside_up_rd),
		.up_wr(iside_up_wr),
		.up_mask(iside_up_mask),
		.up_addr(iside_up_addr),
		.up_din(iside_up_din),
		.up_dout(iside_up_dout),
		.up_doutok(iside_up_doutok),
		.dn_rd(dside_dn_rd),
		.dn_wr(dside_dn_wr),
		.dn_mask(dside_dn_mask),
		.dn_addr(dside_dn_addr), 
		.dn_din(dside_dn_din),
		.dn_dout(dside_dn_dout),
		.dn_dinok(dside_dn_dinok)
	);
	
	// DSIDE CACHE
	reg dside_up_rd;
	reg dside_up_wr;
	reg[3:0] dside_up_mask;
	reg[31:0] dside_up_addr;
	reg[31:0] dside_up_din;
	wire[31:0] dside_up_dout;
	wire[31:0] dside_up_doutok;
	wire dside_dn_rd;
	wire dside_dn_wr;
	wire[3:0] dside_dn_mask;
	reg[31:0] dside_dn_din;
	wire[31:0] dside_dn_addr;
	wire[31:0] dside_dn_dout;
	reg[31:0] dside_dn_dinok;
	twosetcache dside(
		.clk(clk),
		.up_rd(dside_up_rd),
		.up_wr(dside_up_wr),
		.up_mask(dside_up_mask),
		.up_addr(dside_up_addr),
		.up_din(dside_up_din),
		.up_dout(dside_up_dout),
		.up_doutok(dside_up_doutok),
		.dn_rd(dside_dn_rd),
		.dn_wr(dside_dn_wr),
		.dn_mask(dside_dn_mask),
		.dn_addr(dside_dn_addr), 
		.dn_din(dside_dn_din),
		.dn_dout(dside_dn_dout),
		.dn_dinok(dside_dn_dinok)
	);

endmodule
